Async Runtimes
Why do we need an async runtime
Rust gives you: async fn, Future, .await
But Rust does not give you: A scheduler, An event loop, IO drivers, Timers
This is intentional.
Rust async is runtime-agnostic.
So this code:
async fn hello() {
println!("hello");
}
fn main() {
hello(); // creates a future, does not run it
}
Needs something to:
- Poll the future
- Wake it when it can make progress
- Manage IO and timers
That “something” is an async runtime.
What is an async runtime?
An async runtime is a library that provides:
An executor + IO reactor + timer + task scheduler
All working together.
Conceptually:
┌─────────────┐
│ Executor │
└──────┬──────┘
│ polls
┌──────▼──────┐
│ Futures │
└──────┬──────┘
│ register wakers
┌──────▼──────┐
│ Reactor │ ← epoll / kqueue / IOCP
└──────┬──────┘
│ wake
┌──────▼──────┐
│ Scheduler │
└─────────────┘
Core components of an async runtime
Executor
- Polls futures
- Stops polling when they return
Pending - Resumes them when woken
This is the heart of async.
Scheduler
Decides:
- Which task runs next
- On which thread
- When to yield
Schedulers can be:
- Single-threaded
- Multi-threaded (work stealing)
Reactor (IO driver)
Listens for:
- Socket readiness
- File IO completion
- OS events
Uses OS primitives:
- Linux:
epoll - macOS:
kqueue - Windows:
IOCP
Timer system
Handles: sleep, timeout, interval-based tasks
Tokio runtime (industry standard)
Tokio is:
- High performance
- Highly configurable
- Production-grade
- Used by: AWS, Discord, Cloudflare, etc.
Tokio architecture
- Multi-threaded work-stealing scheduler
- Separate IO reactor
- Very explicit APIs
Example: Basic Tokio runtime
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("start");
sleep(Duration::from_secs(1)).await;
println!("end");
}
What #[tokio::main] does
It expands to roughly:
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
// your async main
});
}
Tokio task spawning and scheduling
use tokio::time::{sleep, Duration};
async fn worker(id: u32) {
println!("worker {} started", id);
sleep(Duration::from_secs(1)).await;
println!("worker {} finished", id);
}
#[tokio::main]
async fn main() {
for i in 1..=3 {
tokio::spawn(worker(i));
}
sleep(Duration::from_secs(2)).await;
}
tokio::spawncreates a task- Tasks are scheduled across runtime threads
.awaityields control cooperatively
Blocking vs non-blocking in Tokio
Blocking (bad)
std::thread::sleep(Duration::from_secs(1));
This blocks a runtime worker thread.
Correct way
tokio::time::sleep(Duration::from_secs(1)).await;
CPU-bound work
tokio::task::spawn_blocking(|| {
heavy_computation();
});
This moves blocking work to a dedicated thread pool.
async-std runtime (stdlib-like design)
async-std aims to feel like Rust’s standard library.
Philosophy: Simple, Familiar APIs, Less configuration, Opinionated defaults
async-std example
use async_std::task;
use std::time::Duration;
async fn work() {
println!("working...");
task::sleep(Duration::from_secs(1)).await;
println!("done");
}
fn main() {
task::block_on(work());
}
Key differences
- No macros required
block_onexplicitly starts runtime- API mirrors
std::thread
Spawning tasks in async-std
use async_std::task;
async fn worker(id: u32) {
println!("worker {} started", id);
task::sleep(std::time::Duration::from_secs(1)).await;
println!("worker {} finished", id);
}
fn main() {
task::block_on(async {
for i in 1..=3 {
task::spawn(worker(i));
}
});
}
Tokio vs async-std (practical comparison)
| Feature | Tokio | async-std |
|---|---|---|
| Performance | Very high | Good |
| Ecosystem | Huge | Smaller |
| Configuration | Very flexible | Minimal |
| API style | Explicit | std-like |
| Industry use | Dominant | Moderate |
| Learning curve | Steeper | Easier |
Mixing runtimes (don’t)
- Tokio futures expect Tokio reactor
- async-std futures expect async-std reactor
- Mixing causes deadlocks or panics
Use one runtime per application.
When to choose which runtime
Choose Tokio if:
- You’re building servers
- You need max performance
- You depend on popular crates (hyper, tonic, axum)
Choose async-std if:
- You want simplicity
- You’re building small tools
- You like std-like APIs
Mental model summary
An async runtime is:
A loop that keeps polling futures, listens to the OS for events, and wakes tasks when progress is possible.
Or simpler:
“The engine that makes
.awaitactually work.”